package daybook.model;

import daybook.util.ImmutablePair;
import daybook.util.Pair;
import daybook.util.Utils;
import daybook.util.tree.Tree;

import java.util.*;

// code <-> Account
// full name <-> Account
// alias <-> Account
// Account -> sub accounts, Account -> parent account
public final class ChartOfAccounts {
    private final Tree<Account> accountTree;
    private final Map<String, Tree<Account>> nameToAccountTree;
    private final Map<String, String> codeToFullName;
    private final String accountNameSeparator;

    private ChartOfAccounts(Tree<Account> accountTree,
                            Map<String, Tree<Account>> nameToAccountTree,
                            Map<String, String> codeToFullName,
                            String accountNameSeparator) {
        this.accountTree = accountTree;
        this.nameToAccountTree = nameToAccountTree;
        this.codeToFullName = codeToFullName;
        this.accountNameSeparator = accountNameSeparator;
    }

//    private static void buildIndexes(List<Tree<Account>> trees, String accountNameSeparator, String prefix, Map<String, Account> nameToAccount, Map<String, String> codeToFullName) {
//        for (Tree<Account> tree: trees) {
//            final Account account = tree.getValue();
//            // code
//            Utils.putUniquely(nameToAccount, account.getCode(), account);
//            // alias
//            for (String alias: account.getAliases()) {
//                Utils.putUniquely(nameToAccount, alias, account);
//            }
//            // fullName
//            final String fullName = prefix + account.getName();
//            Utils.putUniquely(nameToAccount, fullName, account);
//            Utils.putUniquely(codeToFullName, account.getCode(), fullName);
//
//            buildIndexes(tree.getChildren(), accountNameSeparator, fullName + accountNameSeparator, nameToAccount, codeToFullName);
//        }
//    }

    public static ChartOfAccounts of(Tree<Account> tree, String accountNameSeparator) {
        final Map<String, Tree<Account>> nameToAccountTree = new HashMap<>();
        final Map<String, String> codeToFullName = new HashMap<>();

        // stack of pairs of accountTree and fullName
        final Deque<Pair<Tree<Account>, String>> stack = new ArrayDeque<>();
        for (Tree<Account> topLevel: tree.getChildren()) {
            stack.push(ImmutablePair.of(topLevel, ""));
        }
        while (!stack.isEmpty()) {
            final Pair<Tree<Account>, String> pair = stack.pop();
            final Tree<Account> accountTree = pair.first();
            // code
            Utils.putUniquely(nameToAccountTree, accountTree.getValue().getCode(), accountTree);
            // alias
            for (String alias: accountTree.getValue().getAliases()) {
                Utils.putUniquely(nameToAccountTree, alias, accountTree);
            }
            // fullName
            final String fullName = pair.second() + accountTree.getValue().getName();
            Utils.putUniquely(nameToAccountTree, fullName, accountTree);
            Utils.putUniquely(codeToFullName, accountTree.getValue().getCode(), fullName);

            for (Tree<Account> a: pair.first().getChildren()) {
                stack.push(ImmutablePair.of(a, fullName + accountNameSeparator));
            }
        }
//        buildIndexes(tree.getChildren(), accountNameSeparator, "", nameToAccount, codeToFullName);

        return new ChartOfAccounts(tree, nameToAccountTree, codeToFullName, accountNameSeparator);
    }

    public Tree<Account> getTree() {
        return accountTree;
    }

    public Optional<String> findFullNameByCode(String code) {
        return Optional.ofNullable(codeToFullName.get(code));
    }

    public Optional<Tree<Account>> findTreeByCode(String code) {
        return Optional.ofNullable(nameToAccountTree.get(code))
                .filter(accountTree -> accountTree.getValue().getCode().equals(code));
    }

    public Optional<Tree<Account>> findTreeByFullName(String fullName) {
        if (fullName.contains(accountNameSeparator)) {
            return Optional.ofNullable(nameToAccountTree.get(fullName));
        } else {
            return Optional.ofNullable(nameToAccountTree.get(fullName))
                    .filter(accountTree -> {
                                final Account account = accountTree.getValue();
                                return account.getParentCode().equals(Account.ROOT.getCode()) &&
                                        !account.getCode().equals(fullName) &&
                                        !account.getAliases().contains(fullName);
                            });
        }
    }

    public Optional<Tree<Account>> findTreeByAlias(String alias) {
        return Optional.ofNullable(nameToAccountTree.get(alias))
                .filter(accountTree -> accountTree.getValue().getAliases().contains(alias));
    }

    public Optional<Tree<Account>> findTree(String name) {
        return Optional.ofNullable(nameToAccountTree.get(name));
    }

    public Optional<Account> findByFullName(String fullName) {
        return findTreeByFullName(fullName).map(Tree::getValue);
    }

    public Optional<Account> findByCode(String code) {
        return findTreeByCode(code).map(Tree::getValue);
    }

    public Optional<Account> findByAlias(String alias) {
        return findTreeByAlias(alias).map(Tree::getValue);
    }

    public Optional<Account> find(String name) {
        return findTree(name).map(Tree::getValue);
    }

    @Override
    public String toString() {
        return "ChartOfAccounts{" +
                "accountTree=" + accountTree +
                ", nameToAccountTree=" + nameToAccountTree +
                ", codeToFullName=" + codeToFullName +
                ", accountNameSeparator='" + accountNameSeparator + '\'' +
                '}';
    }
}
